/* Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gdata.aeuploader;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Scanner;
import java.util.logging.Logger;


/**
 * AppEngine compatible Resumable Uploader.
 */
public class Uploader {

  /** Blob provider. */
  private BlobProvider blob;
  
  /** The authorization header. */
  private String auth;
  
  /** Meta string. */
  private String meta;
  
  /** Initial upload url. */
  private URL initialUrl;
  
  /** Chunk size. */
  private int chunk;
  
  /** Constant LOGGER. */
  private final static Logger LOGGER = Logger.getLogger(Uploader.class.getName());
  
  /** Constant XLENGTH_HEADER. */
  private static final String XLENGTH_HEADER = "X-Upload-Content-Length";
  
  /** Constant XTYPE_HEADER. */
  private static final String XTYPE_HEADER = "X-Upload-Content-Type";
  
  /** Constant CTYPE_HEADER. */
  private static final String CTYPE_HEADER = "Content-type";
  
  /** Constant CRANGE_HEADER. */
  private static final String CRANGE_HEADER = "Content-range";

  /** Constant DEFAULT_CHUNK_SIZE. */
  public static final int DEFAULT_CHUNK_SIZE = 1024 * 1024;

  
  /**
   * Instantiates a new uploader.
   *
   * @param url the url
   * @param authHeader the auth header
   * @param blobProvider the blob provider
   * @param metaData the meta data
   * @param chunkSize the chunk size
   */
  public Uploader(URL url, String authHeader, BlobProvider blobProvider,
      String metaData, int chunkSize) {
    initialUrl = url;
    auth = authHeader;
    meta = metaData;
    blob = blobProvider;
    chunk = chunkSize;
  }
  
  /**
   * Instantiates a new uploader.
   *
   * @param url the url
   * @param authHeader the auth header
   * @param blobProvider the blob provider
   * @param chunkSize the chunk size
   */
  public Uploader(URL url, String authHeader, BlobProvider blobProvider,
      int chunkSize) {
    this(url, authHeader, blobProvider, null, chunkSize);
  }
  
  /**
   * Instantiates a new uploader.
   *
   * @param url the url
   * @param authHeader the auth header
   * @param blobProvider the blob provider
   * @param metaData the meta data
   */
  public Uploader(URL url, String authHeader, BlobProvider blobProvider,
      String metaData) {
    this(url, authHeader, blobProvider, metaData, DEFAULT_CHUNK_SIZE);
  }

  /**
   * Instantiates a new uploader.
   *
   * @param url the url
   * @param auth the auth
   * @param blob the blob
   */
  public Uploader(URL url, String auth, BlobProvider blob) {
    this(url, auth, blob, null, DEFAULT_CHUNK_SIZE);
  }

  /**
   * Creates the connection with authorization and gdata headers set.
   *
   * @param url the url
   * @param requestType the request type
   * @return the http url connection
   * @throws IOException Signals that an I/O exception has occurred.
   */
  private HttpURLConnection CreateConnection(URL url, String requestType)
      throws IOException {
    HttpURLConnection c = (HttpURLConnection) url.openConnection();
    c.setConnectTimeout(60000);
    c.setReadTimeout(60000);
    c.setRequestMethod(requestType);
    c.setUseCaches(false);
    c.setRequestProperty("Authorization", this.auth);
    c.setRequestProperty("GData-version", "3.0");
    c.setInstanceFollowRedirects(false);
    return c;
  }

  /**
   * Extract location header as a URL from the response.
   *
   * @param c the c
   * @return the uRL
   * @throws IOException Signals that an I/O exception has occurred.
   */
  private URL ExtractLocation(HttpURLConnection c) throws IOException {
    String location = c.getHeaderField("Location");
    LOGGER.fine("Resume location: " + location);
    if (location != null) {
      try {
        return new URL(location);
      } catch (MalformedURLException e) {
        return null;
      }
    } else {
      return null;
    }
  }

  /**
   * Sets the X-Content headers for a connection.
   *
   * @param c the connection
   */
  private void SetXHeaders(HttpURLConnection c) {    
    c.setRequestProperty(XLENGTH_HEADER, Long.toString(blob.getSize()));
    c.setRequestProperty(XTYPE_HEADER, blob.getType());
  }

  /**
   * Writes the content to a connection.
   *
   * @param c the connection
   * @param type the type
   * @param data the data
   * @throws IOException Signals that an I/O exception has occurred.
   */
  private void SetContent(HttpURLConnection c, String type, byte[] data)
      throws IOException {
    c.setRequestProperty(CTYPE_HEADER, type);
    c.setDoOutput(true);
    OutputStream out = c.getOutputStream();
    out.write(data);
    out.close();
  }


  /**
   * Read the response from a connection.
   *
   * @param c the connection
   * @return the string response
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public String ReadConnection(HttpURLConnection c) throws IOException {
    InputStream stream = c.getInputStream();
    return new Scanner(stream).useDelimiter("\\A").next();
  }
  
  
  /**
   * Sets the Range headers on a connection.
   *
   * @param c the connection
   * @param position the position in the stream
   * @param size the size of the data
   */
  public void SetRangeheaders(HttpURLConnection c, long position, int size) {
    c.addRequestProperty(CTYPE_HEADER, blob.getType());
    c.addRequestProperty(CRANGE_HEADER, String.format("bytes %s-%s/%s",
        position, position + size - 1, blob.getSize()));
  }

  /**
   * Make a resumed request.
   *
   * @param url the url
   * @param data the data
   * @param position the position in the stream
   * @return the http url connection
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public HttpURLConnection ResumeRequest(URL url, byte[] data, long position)
      throws IOException {
    HttpURLConnection c = CreateConnection(url, "PUT");
    SetRangeheaders(c, position, data.length);
    SetContent(c, blob.getType(), data);
    LOGGER.fine("Resume: " + url + " " + c.getResponseCode() + " " + 
        c.getHeaderFields());
    return c;
  }

  /**
   * Checks whether this uploader has Metadata.
   *
   * @return true, if there is Metadata
   */
  private boolean hasMeta() {
    return (this.meta != null);
  }
  
  /**
   * Initiate the upload session.
   *
   * @param requestType the request type
   * @return the url for resumed requests
   * @throws IOException Signals that an I/O exception has occurred.
   */
  protected URL InitiateUpload(String requestType) throws IOException {
    HttpURLConnection c = CreateConnection(this.initialUrl, requestType);
    SetXHeaders(c);
    if (hasMeta()) {
      SetContent(c, "application/atom+xml", meta.getBytes());
    } else {
      SetContent(c, blob.getType(), new byte[0]);
    }
    return ExtractLocation(c);
  }
  
  /**
   * Resume an upload.
   *
   * @param url the url
   * @return the string response of the final request
   * @throws IOException Signals that an I/O exception has occurred.
   */
  protected String ResumeUpload(URL url) throws IOException {
    long position = 0;
    byte[] buffer = new byte[chunk];
    HttpURLConnection c = null;
    InputStream stream = blob.getStream();
    while (position < blob.getSize()) {
      int length = stream.read(buffer);
      byte[] data = Arrays.copyOf(buffer, length);
      c = ResumeRequest(url, data, position);
      position += chunk;
    }
    String response = ReadConnection(c);
    LOGGER.fine("Responded: " + response);
    return response;
  }

  /**
   * Perform an entire upload.
   *
   * @param requestType the request type
   * @return the string response of the final request
   * @throws IOException Signals that an I/O exception has occurred.
   */
  protected String Upload(String requestType) throws IOException {
    LOGGER.fine("Upload: " + requestType + " to " + initialUrl);
    URL url = InitiateUpload(requestType);
    return ResumeUpload(url);
  }
  
  /**
   * Creates a new resource.
   *
   * @return the string
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public String Create() throws IOException {
    return Upload("POST");
  }
  
  /**
   * Update an existing resource.
   *
   * @return the string
   * @throws IOException Signals that an I/O exception has occurred.
   */
  public String Update() throws IOException {
    return Upload("PUT");
  }
}
